import type { SourceToken, Token } from '../parse/cst.ts'
import type { ComposeErrorHandler } from './composer.ts'

export interface ResolvePropsArg {
  flow?: 'flow map' | 'flow sequence'
  indicator: 'doc-start' | 'explicit-key-ind' | 'map-value-ind' | 'seq-item-ind'
  next: Token | null | undefined
  offset: number
  onError: ComposeErrorHandler
  parentIndent: number
  startOnNewline: boolean
}

export function resolveProps(
  tokens: SourceToken[],
  {
    flow,
    indicator,
    next,
    offset,
    onError,
    parentIndent,
    startOnNewline
  }: ResolvePropsArg
) {
  let spaceBefore = false
  let atNewline = startOnNewline
  let hasSpace = startOnNewline
  let comment = ''
  let commentSep = ''
  let hasNewline = false
  let reqSpace = false
  let tab: SourceToken | null = null
  let anchor: SourceToken | null = null
  let tag: SourceToken | null = null
  let newlineAfterProp: SourceToken | null = null
  let comma: SourceToken | null = null
  let found: SourceToken | null = null
  let start: number | null = null
  for (const token of tokens) {
    if (reqSpace) {
      if (
        token.type !== 'space' &&
        token.type !== 'newline' &&
        token.type !== 'comma'
      )
        onError(
          token.offset,
          'MISSING_CHAR',
          'Tags and anchors must be separated from the next token by white space'
        )
      reqSpace = false
    }
    if (tab) {
      if (atNewline && token.type !== 'comment' && token.type !== 'newline') {
        onError(tab, 'TAB_AS_INDENT', 'Tabs are not allowed as indentation')
      }
      tab = null
    }
    switch (token.type) {
      case 'space':
        // At the doc level, tabs at line start may be parsed
        // as leading white space rather than indentation.
        // In a flow collection, only the parser handles indent.
        if (
          !flow &&
          (indicator !== 'doc-start' || next?.type !== 'flow-collection') &&
          token.source.includes('\t')
        ) {
          tab = token
        }
        hasSpace = true
        break
      case 'comment': {
        if (!hasSpace)
          onError(
            token,
            'MISSING_CHAR',
            'Comments must be separated from other tokens by white space characters'
          )
        const cb = token.source.substring(1) || ' '
        if (!comment) comment = cb
        else comment += commentSep + cb
        commentSep = ''
        atNewline = false
        break
      }
      case 'newline':
        if (atNewline) {
          if (comment) comment += token.source
          else if (!found || indicator !== 'seq-item-ind') spaceBefore = true
        } else commentSep += token.source
        atNewline = true
        hasNewline = true
        if (anchor || tag) newlineAfterProp = token
        hasSpace = true
        break
      case 'anchor':
        if (anchor)
          onError(
            token,
            'MULTIPLE_ANCHORS',
            'A node can have at most one anchor'
          )
        if (token.source.endsWith(':'))
          onError(
            token.offset + token.source.length - 1,
            'BAD_ALIAS',
            'Anchor ending in : is ambiguous',
            true
          )
        anchor = token
        if (start === null) start = token.offset
        atNewline = false
        hasSpace = false
        reqSpace = true
        break
      case 'tag': {
        if (tag)
          onError(token, 'MULTIPLE_TAGS', 'A node can have at most one tag')
        tag = token
        if (start === null) start = token.offset
        atNewline = false
        hasSpace = false
        reqSpace = true
        break
      }
      case indicator:
        // Could here handle preceding comments differently
        if (anchor || tag)
          onError(
            token,
            'BAD_PROP_ORDER',
            `Anchors and tags must be after the ${token.source} indicator`
          )
        if (found)
          onError(
            token,
            'UNEXPECTED_TOKEN',
            `Unexpected ${token.source} in ${flow ?? 'collection'}`
          )
        found = token
        atNewline =
          indicator === 'seq-item-ind' || indicator === 'explicit-key-ind'
        hasSpace = false
        break
      case 'comma':
        if (flow) {
          if (comma)
            onError(token, 'UNEXPECTED_TOKEN', `Unexpected , in ${flow}`)
          comma = token
          atNewline = false
          hasSpace = false
          break
        }
      // else fallthrough
      default:
        onError(token, 'UNEXPECTED_TOKEN', `Unexpected ${token.type} token`)
        atNewline = false
        hasSpace = false
    }
  }
  const last = tokens[tokens.length - 1]
  const end = last ? last.offset + last.source.length : offset
  if (
    reqSpace &&
    next &&
    next.type !== 'space' &&
    next.type !== 'newline' &&
    next.type !== 'comma' &&
    (next.type !== 'scalar' || next.source !== '')
  ) {
    onError(
      next.offset,
      'MISSING_CHAR',
      'Tags and anchors must be separated from the next token by white space'
    )
  }
  if (
    tab &&
    ((atNewline && tab.indent <= parentIndent) ||
      next?.type === 'block-map' ||
      next?.type === 'block-seq')
  )
    onError(tab, 'TAB_AS_INDENT', 'Tabs are not allowed as indentation')
  return {
    comma,
    found,
    spaceBefore,
    comment,
    hasNewline,
    anchor,
    tag,
    newlineAfterProp,
    end,
    start: start ?? end
  }
}
